#include <alloca.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include <errno.h>
#include <iconv.h>

#include <archive.h>
#include <archive_entry.h>

/* https://www.freedesktop.org/wiki/Software/uchardet/ */
#include <uchardet.h>

#include "common.h"

// #define DEBUG
#include "log.h"

#define ICONV_OUTPUT_BUFSIZ 512
#define WRITER_FLAGS (ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_TIME)

typedef struct {
    struct archive *reader;
    struct archive *writer;
    struct archive_entry *current;
    const char *source;
    char *destdir;
    bool complete;
    bool verbose;
} AR;

static bool ar_prepare(AR *ar, const char *src);
static ssize_t ar_extract_entry(AR *ar);
static void ar_convert_pathname_to_utf8(AR *ar);
static char* ar_first_pass(AR *ar, const char *destdir);

AR *ar_init_extractor(const char *src, const char *destdir, const AROptions *options)
{
    AR *ar;

    ar = (AR*)malloc(sizeof(AR));
    assert(ar != NULL && "Out of memory");
    memset(ar, 0, sizeof(AR));

    ar->source = src;
    ar->verbose = options->verbose;

    if (!ar_prepare(ar, src))
    {
	free(ar);
	return NULL;
    }

    if (options->mode == DIR_MODE_AUTO)
    {
	ar->destdir = ar_first_pass(ar, destdir);
	archive_read_free(ar->reader);
	archive_write_free(ar->writer);

	if (!ar_prepare(ar, src))
	{
	    free(ar->destdir);
	    free(ar);
	    return NULL;
	}
    }
    else
    {
	if (destdir && options->mode == DIR_MODE_ALWAYS)
	{
	    ar->destdir = ex_strdup(destdir);
	}
	else
	{
	    ar->destdir = NULL;
	}
    }

    return ar;
}

void ar_decompress_archive(AR *ar)
{
    ssize_t res;

    if (ar->verbose)
    {
	printf("%s:\n", ar->source);
    }

    while (true) {
        res = archive_read_next_header(ar->reader, &ar->current);
        if (res == ARCHIVE_EOF) {
            break;
        }

        if (NULL != ar->destdir) {
            const char *original;
            char *new_path;
            size_t new_size;

            xprintf(LOG_LEVEL_TRACE, "Prepending ar->destdir");
            original = archive_entry_pathname(ar->current);
            /* Add two so there is enough space for directory separator and terminating zero */
            new_size = strlen(ar->destdir) + strlen(original) + 2;
            new_path = (char*)malloc(new_size);
            memset(new_path, 0, new_size);
            sprintf(new_path, "%s/%s", ar->destdir, original);
            archive_entry_set_pathname(ar->current, new_path);
            free(new_path);
        }

        ar_convert_pathname_to_utf8(ar);

        res = archive_write_header(ar->writer, ar->current);
        if (res == ARCHIVE_WARN) {
            xprintf(LOG_LEVEL_WARNING, archive_error_string(ar->writer));
        } else if (res != ARCHIVE_OK) {
            /* TODO: Handle retries. */
            /* TODO: Notify GUI of error */
            xprintf(LOG_LEVEL_ERROR, "Writing archive header: %s", archive_error_string(ar->writer));
            ar->complete = true;
            return;
        }

	if (archive_entry_size(ar->current) > 0)
	{
	    res = ar_extract_entry(ar);
	    if (res == ARCHIVE_OK && ar->verbose)
	    {
		printf("  %s (%li B)... OK.\n", archive_entry_pathname(ar->current), archive_entry_size(ar->current));
	    }
	}
    }

    if (ar->verbose && ar->destdir)
    {
	printf("Successfully extracted to \"%s\".\n", ar->destdir);
    }
    else if (ar->verbose)
    {
	puts("Successfully extracted to the current directory");
    }

    ar->complete = true;
}

bool ar_end_extracting(AR *ar)
{
    xprintf(LOG_LEVEL_TRACE, "Releasing resources for AR");
    archive_read_free(ar->reader);
    archive_write_free(ar->writer);

    free(ar->destdir);
    free(ar);
    return true;
}

bool ar_check_complete(AR *ar)
{
    /* HACK: Assuming NULL means we've free'd AR
     *       in ar_end_extracting.
     */
    if (NULL == ar) {
        return true;
    }
    return ar->complete;
}

static ssize_t ar_extract_entry(AR *ar)
{
    ssize_t res;
    const void *data;
    size_t data_size;
    off_t data_offset;

    while (true) {
        res = archive_read_data_block(ar->reader, &data, &data_size, &data_offset);
        if (res == ARCHIVE_EOF) {
            return ARCHIVE_OK;
        }
        if (res < ARCHIVE_OK) {
            xprintf(LOG_LEVEL_WARNING, archive_error_string(ar->reader));
            return res;
        }
        res = archive_write_data_block(ar->writer, data, data_size, data_offset);
        if (res < ARCHIVE_OK) {
            xprintf(LOG_LEVEL_ERROR, archive_error_string(ar->writer));
            return res;
        }
    }

    assert(0 && "Unreachable");
}

static void ar_convert_pathname_to_utf8(AR *ar)
{
    static char buffer[ICONV_OUTPUT_BUFSIZ];
    char *output_ptr;
    char *pathname_ptr;

    size_t pathname_length;
    size_t remaining;
    size_t res;

    uchardet_t detector;
    iconv_t converter;

    memset(buffer, 0, sizeof(buffer));
    pathname_length = strlen(archive_entry_pathname(ar->current));

    detector = uchardet_new();
    uchardet_handle_data(detector, archive_entry_pathname(ar->current), pathname_length);
    uchardet_data_end(detector);

    xprintf(LOG_LEVEL_DEBUG, "Detected charset %s", uchardet_get_charset(detector));

    if (strcmp("ASCII", uchardet_get_charset(detector)) == 0) {
        goto uchar_del;
    }
    if (strcmp("UTF-8", uchardet_get_charset(detector)) == 0) {
        goto uchar_del;
    }

    converter = iconv_open("UTF-8", uchardet_get_charset(detector));
    if ((iconv_t)-1 == converter) {
        xprintf(LOG_LEVEL_ERROR, "Failed to open iconv converter: %s", strerror(errno));
        goto uchar_del;
    }

    output_ptr = buffer;
    pathname_ptr = (char *)archive_entry_pathname(ar->current);
    remaining = ICONV_OUTPUT_BUFSIZ;

    res = iconv(converter, &pathname_ptr, &pathname_length, &output_ptr, &remaining);
    if ((size_t)-1 == res) {
        xprintf(LOG_LEVEL_ERROR, "Failed to convert: %s", strerror(errno));
        goto iconv_del;
    }

    archive_entry_copy_pathname(ar->current, buffer);

iconv_del:
    iconv_close(converter);

uchar_del:
    uchardet_delete(detector);
}

static bool ar_prepare(AR *ar, const char *src)
{
    ssize_t res;

    ar->reader = archive_read_new();
    ar->writer = archive_write_disk_new();

    /* Reader configuration */
    archive_read_support_filter_all(ar->reader);
    archive_read_support_format_all(ar->reader);

    /* Writer configuration */
    archive_write_disk_set_options(ar->writer, WRITER_FLAGS);
    archive_write_disk_set_standard_lookup(ar->writer);

    res = archive_read_open_filename(ar->reader, src, 10240);
    if (res != ARCHIVE_OK) {
	xprintf(LOG_LEVEL_ERROR, "Could not open archive: %s", archive_error_string(ar->reader));
	return false;
    }

    return true;
}

static char *ar_first_pass(AR *ar, const char *destdir)
{
    char *first_path = NULL;
    size_t first_path_len = 0;

    while (true)
    {
	const char *current_path;

	ssize_t res = archive_read_next_header(ar->reader, &ar->current);
	if (res == ARCHIVE_EOF)
	{
	    break;
	}

	current_path = archive_entry_pathname(ar->current);

	if (!first_path)
	{
	    char* sep = strchr((char*)current_path, '/');
	    size_t len = (sep == NULL) ? strlen(current_path) : (size_t)(sep - current_path);

	    first_path_len = len;
	    first_path = (char*)alloca(first_path_len);
	    strncpy(first_path, current_path, first_path_len);
	    continue;
	}

	if (strncmp(first_path, current_path, first_path_len) != 0)
	{
	    char *res = (char*)calloc(strlen(destdir), sizeof(char));
	    strcpy(res, destdir);

	    xprintf(LOG_LEVEL_DEBUG, "Mutliple top-level files detected, setting destdir to %s.", res);

	    return res;
	}
    }

    return NULL;
}
